﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="WeakDelegate.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Base class for <see cref="WeakDelegate{T}" />.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Collections;
    using Reflection;

#if SILVERLIGHT
    
    using ReaderWriterLockSlim = Threading.ReaderWriterLockSlim;

#else
    
    using ReaderWriterLockSlim = System.Threading.ReaderWriterLockSlim;

#endif

    /// <summary>
    /// Base class for <see cref="WeakDelegate{T}"/>.
    /// </summary>
    /// <remarks>
    /// The base <see cref="WeakDelegate"/> class provides untyped access to features of the 
    /// <see cref="WeakDelegate{T}"/> type.
    /// </remarks>
    public abstract class WeakDelegate : IWeakObject
    {
        #region Fields

        private readonly WeakReference _targetReference;
        private readonly MethodInfo _method;
        private static readonly MethodInfo CreateMethod = typeof(WeakDelegate)
            .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) .FirstOrDefault(x => x.Name == "Create" && x.IsGenericMethodDefinition);

        #endregion

        internal WeakDelegate(object target, MethodInfo method)
        {
            if (!method.IsStatic)
            {
                _targetReference = new WeakReference(target);
            }
            _method = method;
        }

        internal static WeakDelegate Create<TDelegate>(TDelegate handler) where TDelegate : class
        {
            return (WeakDelegate<TDelegate>)handler;
        }

        public static WeakDelegate Create(Delegate @delegate)
        {
            return (WeakDelegate)CreateMethod.MakeGenericMethod(@delegate.GetType()).Invoke(
                null, new object[] { @delegate });
        }

        public object Target
        {
            get { return _targetReference != null ? _targetReference.Target : null; }
        }

        public MethodInfo Method
        {
            get { return _method; }
        }

        public abstract object DynamicInvoke(params object[] args);

        public ReadOnlyList<WeakDelegate> GetInvocationList(bool flush)
        {
            return new ReadOnlyList<WeakDelegate>(GetInvocationListCore(flush));
        }

        public virtual bool QueryIsAlive(bool flush)
        {
            return _method.IsStatic || _targetReference.IsAlive;
        }

        protected abstract WeakDelegate[] GetInvocationListCore(bool flush);
    }

    public class WeakDelegate<TDelegate> : WeakDelegate, IDelegatable where TDelegate : class
    {
        #region Fields

        private Delegate _fallback;
        private TDelegate _invoke;

        #endregion

        internal WeakDelegate(object target, MethodInfo method)
            : base(target, method)
        {
            WeakDelegateRuntime.Binder<TDelegate>.EnsureInitialized();
            StaticInvoke = StaticInvokeCache.GetOrCreateDelegate(
                WeakDelegateRuntime.Binder<TDelegate>.InvokeMethod, method);
        }

        private Delegate StaticInvoke { get; set; }

        public TDelegate Invoke
        {
            get { return _invoke ?? (_invoke = WeakDelegateRuntime.Binder<TDelegate>.Bind(this)); }
        }

        #region IDelegatable Members

        void IDelegatable.InvokeAction()
        {
            OnInvokeAction();
        }

        void IDelegatable.InvokeAction<TArg1>(TArg1 arg1)
        {
            OnInvokeAction(arg1);
        }

        void IDelegatable.InvokeAction<TArg1, TArg2>(TArg1 arg1, TArg2 arg2)
        {
            OnInvokeAction(arg1, arg2);
        }

        void IDelegatable.InvokeAction<TArg1, TArg2, TArg3>(TArg1 arg1, TArg2 arg2, TArg3 arg3)
        {
            OnInvokeAction(arg1, arg2, arg3);
        }

        void IDelegatable.InvokeAction<TArg1, TArg2, TArg3, TArg4>(TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4)
        {
            OnInvokeAction(arg1, arg2, arg3, arg4);
        }

        TReturn IDelegatable.InvokeFunc<TReturn>()
        {
            return OnInvokeFunc<TReturn>();
        }

        TReturn IDelegatable.InvokeFunc<TArg1, TReturn>(TArg1 arg1)
        {
            return OnInvokeFunc<TArg1, TReturn>(arg1);
        }

        TReturn IDelegatable.InvokeFunc<TArg1, TArg2, TReturn>(TArg1 arg1, TArg2 arg2)
        {
            return OnInvokeFunc<TArg1, TArg2, TReturn>(arg1, arg2);
        }

        TReturn IDelegatable.InvokeFunc<TArg1, TArg2, TArg3, TReturn>(TArg1 arg1, TArg2 arg2, TArg3 arg3)
        {
            return OnInvokeFunc<TArg1, TArg2, TArg3, TReturn>(arg1, arg2, arg3);
        }

        TReturn IDelegatable.InvokeFunc<TArg1, TArg2, TArg3, TArg4, TReturn>(TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4)
        {
            return OnInvokeFunc<TArg1, TArg2, TArg3, TArg4, TReturn>(arg1, arg2, arg3, arg4);
        }

        #endregion

        public virtual void UseFallback(TDelegate value)
        {
            if (value != null)
            {
                var fallbackDelegate = (Delegate)(object)value;
                if (!fallbackDelegate.Method.IsStatic)
                {
                    throw new ArgumentException("Fallback method must be static.");
                }

                _fallback = StaticInvokeCache.GetOrCreateDelegate(WeakDelegateRuntime.Binder<TDelegate>
                    .InvokeMethod, fallbackDelegate.Method);
            }
            else
            {
                _fallback = null;
            }
        }

        public new ReadOnlyList<WeakDelegate<TDelegate>> GetInvocationList(bool flush)
        {
            return new ReadOnlyList<WeakDelegate<TDelegate>>(GetTypedInvocationListCore(flush));
        }

        protected bool TryGetInvokeAndTarget<T>(out T invoke, out object target) where T : class
        {
            target = Target;
            if (Method.IsStatic || !ReferenceEquals(target, null))
            {
                invoke = (T)(object)StaticInvoke;
            }
            else
            {
                invoke = (T)(object)_fallback;
            }
            return invoke != null;
        }

        protected virtual void OnInvokeAction()
        {
            object target;
            Action<object> invoke;
            if (TryGetInvokeAndTarget(out invoke, out target))
            {
                invoke(target);
            }
        }

        protected virtual void OnInvokeAction<TArg1>(TArg1 arg1)
        {
            object target;
            Action<object, TArg1> invoke;
            if (TryGetInvokeAndTarget(out invoke, out target))
            {
                invoke(target, arg1);
            }
        }

        protected virtual void OnInvokeAction<TArg1, TArg2>(TArg1 arg1, TArg2 arg2)
        {
            object target;
            Action<object, TArg1, TArg2> invoke;
            if (TryGetInvokeAndTarget(out invoke, out target))
            {
                invoke(target, arg1, arg2);
            }
        }

        protected virtual void OnInvokeAction<TArg1, TArg2, TArg3>(TArg1 arg1, TArg2 arg2, TArg3 arg3)
        {
            object target;
            Action<object, TArg1, TArg2, TArg3> invoke;
            if (TryGetInvokeAndTarget(out invoke, out target))
            {
                invoke(target, arg1, arg2, arg3);
            }
        }

        protected virtual void OnInvokeAction<TArg1, TArg2, TArg3, TArg4>(TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4)
        {
            object target;
            Action<object, TArg1, TArg2, TArg3, TArg4> invoke;
            if (TryGetInvokeAndTarget(out invoke, out target))
            {
                invoke(target, arg1, arg2, arg3, arg4);
            }
        }

        protected virtual TReturn OnInvokeFunc<TReturn>()
        {
            object target; Func<object, TReturn> invoke;
            return TryGetInvokeAndTarget(out invoke, out target) ? invoke(target) : default(TReturn);
        }

        protected virtual TReturn OnInvokeFunc<TArg1, TReturn>(TArg1 arg1)
        {
            object target;
            Func<object, TArg1, TReturn> invoke;
            return TryGetInvokeAndTarget(out invoke, out target) ? invoke(target, arg1) : default(TReturn);
        }

        protected virtual TReturn OnInvokeFunc<TArg1, TArg2, TReturn>(TArg1 arg1, TArg2 arg2)
        {
            object target;
            Func<object, TArg1, TArg2, TReturn> invoke;
            return TryGetInvokeAndTarget(out invoke, out target) ? invoke(target, arg1, arg2) : default(TReturn);
        }

        protected virtual TReturn OnInvokeFunc<TArg1, TArg2, TArg3, TReturn>(TArg1 arg1, TArg2 arg2, TArg3 arg3)
        {
            object target;
            Func<object, TArg1, TArg2, TArg3, TReturn> invoke;
            return TryGetInvokeAndTarget(out invoke, out target) ? invoke(target, arg1, arg2, arg3) : default(TReturn);
        }

        protected virtual TReturn OnInvokeFunc<TArg1, TArg2, TArg3, TArg4, TReturn>(TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4)
        {
            object target;
            Func<object, TArg1, TArg2, TArg3, TArg4, TReturn> invoke;
            return TryGetInvokeAndTarget(out invoke, out target) ? invoke(target, arg1, arg2, arg3, arg4) : default(TReturn);
        }

        protected virtual WeakDelegate<TDelegate>[] GetTypedInvocationListCore(bool flush)
        {
            return QueryIsAlive(flush) ? new[] { this } : EmptyArray<WeakDelegate<TDelegate>>.Instance;
        }

        protected override sealed WeakDelegate[] GetInvocationListCore(bool flush)
        {
            return GetTypedInvocationListCore(flush);
        }

        public override object DynamicInvoke(params object[] args)
        {
            return ((Delegate)(object)Invoke).DynamicInvoke(args);
        }

        public static implicit operator TDelegate(WeakDelegate<TDelegate> rhs)
        {
            return rhs.Invoke;
        }

        public static implicit operator WeakDelegate<TDelegate>(TDelegate rhs)
        {
            if (rhs == null)
            {
                return null;
            }

            var result = WeakDelegateRuntime.CastToDelegate(rhs).Target as WeakDelegate<TDelegate>;
            if (result == null)
            {
                result += rhs;
            }

            return result;
        }

        public static WeakDelegate<TDelegate> operator +(WeakDelegate<TDelegate> lhs, TDelegate rhs)
        {
            var rhsDelegate = WeakDelegateRuntime.CastToDelegate(rhs);
            if (lhs == null && rhsDelegate == null)
            {
                return null;
            }

            if (rhsDelegate == null)
            {
                return lhs;
            }

            // get lhs & rhs invocation lists
            var rhsInvocationList = rhsDelegate.GetInvocationList();
            var lhsInvocationList = lhs != null
                ? lhs.GetTypedInvocationListCore(true)
                : EmptyArray<WeakDelegate<TDelegate>>.Instance;

            // if its a single item the just return a non-multi cast WeakDelegate
            if (lhsInvocationList.Length == 0 && rhsInvocationList.Length == 1)
            {
                return new WeakDelegate<TDelegate>(rhsDelegate.Target, rhsDelegate.Method);
            }

            // allocate enough space for combined lhs and rhs invocation lists
            var invocationList = new WeakDelegate<TDelegate>[lhsInvocationList.Length + rhsInvocationList.Length];

            // copy lhs and rhs invocation lists into the new
            var targetIndex = 0;
            foreach (var invocationListEntry in lhsInvocationList)
            {
                invocationList[targetIndex++] = invocationListEntry;
            }
            foreach (var invocationListEntry in rhsInvocationList.Select(x => new WeakDelegate<TDelegate>(x.Target, x.Method)))
            {
                invocationList[targetIndex++] = invocationListEntry;
            }

            // return new multi-cast weak delegate
            return new MulticastWeakDelegate<TDelegate>(invocationList);
        }

        public static WeakDelegate<TDelegate> operator -(WeakDelegate<TDelegate> lhs, TDelegate rhs)
        {
            // cast to delegate type
            var rhsDelegate = WeakDelegateRuntime.CastToDelegate(rhs);
            if (lhs == null)
            {
                return null;
            }

            if (rhs == null)
            {
                return lhs;
            }

            // get lhs invocation list
            var lhsInvocationList = lhs.GetTypedInvocationListCore(true);
            if (lhsInvocationList.Length == 0)
            {
                return null;
            }

            // get rhs invocation list
            var rhsInvocationList = rhsDelegate.GetInvocationList();

            // copy to new list so we can remove items
            var newLhsInvocationList = lhsInvocationList.ToList();

            // remove from end
            for (var i = 0; i < rhsInvocationList.Length && newLhsInvocationList.Count > 0; i++)
            {
                var rhsInvocation = rhsInvocationList[i];
                for (var j = newLhsInvocationList.Count - 1; j >= 0; j--)
                {
                    if (ReferenceEquals(newLhsInvocationList[j].Target, rhsInvocation.Target) &&
                        newLhsInvocationList[j].Method == rhsInvocation.Method)
                    {
                        newLhsInvocationList.RemoveAt(j);
                        break;
                    }
                }
            }

            if (newLhsInvocationList.Count == 0)
            {
                return null;
            }

            if (newLhsInvocationList.Count == 1)
            {
                return newLhsInvocationList[0];
            }

            if (newLhsInvocationList.Count == lhsInvocationList.Length)
            {
                return lhs;
            }

            return new MulticastWeakDelegate<TDelegate>(newLhsInvocationList.ToArray());
        }
    }

    /// <summary>
    /// The StaticInvokeCache maintains a cache of compiled delegates that can be used to invoke instance methods statically.
    /// </summary>
    /// <remarks>
    /// Given an instance method <c>void MyType.Action(int arg1)</c>, the StaticInvokeCache will return the following compiled action.
    /// <code>
    /// Action&lt;object, int&gt; action = (object, arg1) => ((MyType)instance).Action(arg1);
    /// </code>
    /// The <c>sourceMethod</c> parameter is used to convert the resulting delegate type (Contravariance), this is equivalent to:
    /// <code>
    /// public void MyMethod(IList list)
    /// {
    /// }
    /// 
    /// // The following is legal as MyMethod can be invoked with argument <c>object[]</c> which implements <c>IList</c>.
    /// Action&lt;object[]&gt; a = MyMethod
    /// </code> 
    /// </remarks>
    internal class StaticInvokeCache
    {
        #region Fields

        private static readonly Dictionary<Key, Delegate> Cache = new Dictionary<Key, Delegate>();
        private static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim();

        #endregion

        private class Key
        {
            public Key(MethodInfo sourceMethod, MethodInfo targetMethod)
            {
                TargetMethod = targetMethod;
                SourceMethod = sourceMethod;
            }

            private MethodInfo TargetMethod
            {
                get;
                set;
            }

            private MethodInfo SourceMethod
            {
                get;
                set;
            }

            private bool Equals(Key other)
            {
                if (ReferenceEquals(null, other))
                {
                    return false;
                }
                if (ReferenceEquals(this, other))
                {
                    return true;
                }
                return Equals(other.TargetMethod, TargetMethod) && Equals(other.SourceMethod, SourceMethod);
            }

            public override bool Equals(object obj)
            {
                return Equals(obj as Key);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return (TargetMethod.GetHashCode() * 397) ^ (SourceMethod.GetHashCode());
                }
            }
        }

        private static Delegate GetDelegate(Key key)
        {
            Delegate result;
            Lock.EnterReadLock();
            Cache.TryGetValue(key, out result);
            Lock.ExitReadLock();
            return result;
        }

        /// <summary>
        /// Gets a delegate that can be used to statically invoke the specified <paramref name="targetMethod"/> using the 
        /// parameter types defined by the <paramref name="sourceMethod"/>.
        /// </summary>
        /// <param name="sourceMethod">The source method that describes how the method will be invoked.</param>
        /// <param name="targetMethod">The target method that describes how the method will be invoked.</param>
        /// <returns>A delegate that can be used to statically invoke the specified <paramref name="targetMethod"/> using the 
        /// parameter types defined by the <paramref name="sourceMethod"/>.</returns>
        /// <remarks>
        /// The method returned is in the form Invoke(object instance, TArg2 arg0, TArg2 arg1,...TArgN argN).
        /// </remarks>
        internal static Delegate GetDelegate(MethodInfo sourceMethod, MethodInfo targetMethod)
        {
            return GetDelegate(new Key(sourceMethod, targetMethod));
        }

        /// <summary>
        /// Gets or creates a delegate that can be used to statically invoke the specified <paramref name="targetMethod"/> using the 
        /// parameter types defined by the <paramref name="sourceMethod"/>.
        /// </summary>
        /// <param name="sourceMethod">The source method that describes how the method will be invoked.</param>
        /// <param name="targetMethod">The target method that describes how the method will be invoked.</param>
        /// <returns>A delegate that can be used to statically invoke the specified <paramref name="targetMethod"/> using the 
        /// parameter types defined by the <paramref name="sourceMethod"/>.</returns>
        /// <remarks>
        /// The method returned is in the form Invoke(object instance, TArg2 arg0, TArg2 arg1,...TArgN argN).
        /// </remarks>
        internal static Delegate GetOrCreateDelegate(MethodInfo sourceMethod, MethodInfo targetMethod)
        {
            var key = new Key(sourceMethod, targetMethod);
            var result = GetDelegate(key);
            if (result == null)
            {
                result = MethodInfoExtensions.CreateDelegate(typeof(object), sourceMethod, targetMethod);
                Lock.EnterWriteLock();
                Cache[key] = result;
                Lock.ExitWriteLock();
            }

            return result;
        }
    }
}